DefaultEntityMapping.java
package org.codefilarete.stalactite.mapping;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.codefilarete.reflection.AccessorDefinition;
import org.codefilarete.reflection.Accessors;
import org.codefilarete.reflection.PropertyAccessPoint;
import org.codefilarete.reflection.PropertyMutator;
import org.codefilarete.reflection.ReadWritePropertyAccessPoint;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.stalactite.mapping.RowTransformer.TransformerListener;
import org.codefilarete.stalactite.mapping.id.assembly.SingleIdentifierAssembler;
import org.codefilarete.stalactite.mapping.id.manager.IdentifierInsertionManager;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.Duo;
import org.codefilarete.tool.Reflections;
import org.codefilarete.tool.collection.KeepOrderMap;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.Converter;
/**
* <p>
* Main class for persistence entity mapping description.
* Composed of:
* <ul>
* <li>a main strategy : an embedded one ({@link EmbeddedClassMapping}, accessible by {@link #getMapping()}</li>
* <li>an id strategy : {@link SimpleIdMapping} accessible with {@link #getIdMapping()}</li>
* <li>optional version mapping : accessible with {@link #getVersionedKeys()} for instance</li>
* </ul>
* </p>
*
* <p>
* Mapping definition can be eased thanks to {@link PersistentFieldHarvester} or {@link Accessors}
* </p>
*
* <br/>
* <b>THIS CLASS DOESN'T ADDRESS RELATION MAPPING</b>, because it's not the purpose of Stalactite 'core' module, see 'orm' module.
* Meanwhile one can use {@link org.codefilarete.stalactite.query.model.Query} to construct complex type.
*
* @author Guillaume Mary
* @see org.codefilarete.stalactite.query.model.Query
*/
public class DefaultEntityMapping<C, I, T extends Table<T>> implements EntityMapping<C, I, T> {
private final EmbeddedClassMapping<C, T> mapping;
private final Set<Column<T, ?>> insertableColumns = new KeepOrderSet<>();
private final Set<Column<T, ?>> updatableColumns = new KeepOrderSet<>();
private final Set<Column<T, ?>> selectableColumns = new KeepOrderSet<>();
private final IdMapping<C, I> idMapping;
@Nullable
private final Duo<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>> versioningMapping;
private final boolean identifierSetByBeanFactory;
/**
* Main constructor
* Oriented for single column identifier / primary key. Prefer {@link #DefaultEntityMapping(Class, Table, Map, IdMapping)} for composed id.
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param identifierProperty identifier of the persisted class
* @param identifierInsertionManager manager of identifiers
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReadWritePropertyAccessPoint<C, ?>, ? extends Column<T, ?>> propertyToColumn,
ReadWritePropertyAccessPoint<C, I> identifierProperty,
IdentifierInsertionManager<C, I> identifierInsertionManager) {
if (identifierProperty == null) {
throw new UnsupportedOperationException("No identifier property for " + Reflections.toString(classToPersist));
}
if (targetTable.getPrimaryKey() == null) {
throw new UnsupportedOperationException("No primary key column defined for " + targetTable.getAbsoluteName());
}
this.mapping = new EmbeddedClassMapping<>(classToPersist, targetTable, propertyToColumn);
// identifierAccessor must be the same instance as those stored in propertyToColumn for Map.remove method used in foreach()
Column<T, I> identifierColumn = (Column<T, I>) propertyToColumn.get(identifierProperty);
if (identifierColumn == null) {
throw new IllegalArgumentException("Bean identifier '" + AccessorDefinition.toString(identifierProperty) + "' must have its matching column in the mapping");
}
if (!identifierColumn.isPrimaryKey()) {
throw new UnsupportedOperationException("Accessor '" + AccessorDefinition.toString(identifierProperty)
+ "' is declared as identifier but mapped column " + identifierColumn + " is not the primary key of table");
}
this.idMapping = new SimpleIdMapping<>(identifierProperty, identifierInsertionManager, new SingleIdentifierAssembler<>(identifierColumn));
this.identifierSetByBeanFactory = false;
this.versioningMapping = null;
fillInsertableColumns();
fillUpdatableColumns();
fillSelectableColumns();
}
/**
* Secondary constructor, for composed id because one can precisely define the {@link IdMapping} by giving a {@link ComposedIdMapping}
* for instance.
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param idMapping mapping strategy of class identifier
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReadWritePropertyAccessPoint<C, ?>, ? extends Column<T, ?>> propertyToColumn,
IdMapping<C, I> idMapping) {
this(classToPersist, targetTable, propertyToColumn, Collections.emptyMap(), null, idMapping, null, false);
}
/**
* Constructor that let caller give a bean factory. The bean factory is given a value provider of the form of a {@link Function<Column, Object>}
* that can be called to get values to build expected entity or determine the right type to be instantiated according to current row values.
*
* @param classToPersist the class to be persisted
* @param targetTable the persisting table
* @param propertyToColumn mapping between bean "properties" and table columns
* @param idMapping mapping strategy of class identifier
* @param entityFactory entity factory
*/
public DefaultEntityMapping(Class<C> classToPersist,
T targetTable,
Map<? extends ReadWritePropertyAccessPoint<C, ?>, ? extends Column<T, ?>> propertyToColumn,
Map<? extends PropertyMutator<C, ?>, ? extends Column<T, ?>> readonlyColumns,
@Nullable Duo<? extends ReadWritePropertyAccessPoint<C, ?>, ? extends Column<T, ?>> versioningMapping,
IdMapping<C, I> idMapping,
@Nullable Function<ColumnedRow, C> entityFactory,
boolean identifierSetByBeanFactory) {
if (idMapping.getIdAccessor() == null) {
throw new UnsupportedOperationException("No identifier property defined for " + Reflections.toString(classToPersist));
}
if (targetTable.getPrimaryKey() == null) {
throw new UnsupportedOperationException("No primary key column defined for " + targetTable.getAbsoluteName());
}
// adding versioning columns to properties for reading
Map<PropertyMutator<C, ?>, Column<T, ?>> readonlyPropertiesMapping = new KeepOrderMap<>(readonlyColumns);
if (versioningMapping != null) {
readonlyPropertiesMapping.put(versioningMapping.getLeft(), versioningMapping.getRight());
}
this.mapping = new EmbeddedClassMapping<>(classToPersist, targetTable, propertyToColumn, readonlyPropertiesMapping, entityFactory);
this.idMapping = idMapping;
this.identifierSetByBeanFactory = identifierSetByBeanFactory;
this.versioningMapping = (Duo<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>>) versioningMapping;
fillInsertableColumns();
fillUpdatableColumns();
fillSelectableColumns();
}
public Class<C> getClassToPersist() {
return mapping.getClassToPersist();
}
@Override
public T getTargetTable() {
return mapping.getTargetTable();
}
public EmbeddedClassMapping<C, T> getMapping() {
return mapping;
}
/**
* Implementation which returns all properties mapping, even embedded ones.
* The Result is built dynamically.
*
* @return all properties mapping, even embedded ones
*/
@Override
public Map<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>> getPropertyToColumn() {
return new KeepOrderMap<>(getMapping().getPropertyToColumn());
}
@Override
public Map<PropertyMutator<C, ?>, Column<T, ?>> getReadonlyPropertyToColumn() {
return new KeepOrderMap<>(getMapping().getReadonlyPropertyToColumn());
}
@Override
@Nullable
public Duo<ReadWritePropertyAccessPoint<C, ?>, Column<T, ?>> getVersioningMapping() {
return versioningMapping;
}
@Override
public ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> getReadConverters() {
return mapping.getReadConverters();
}
@Override
public ValueAccessPointMap<C, Converter<Object, Object>, PropertyAccessPoint<C, ?>> getWriteConverters() {
return mapping.getWriteConverters();
}
/**
* Gives columns that can be inserted: columns minus generated keys
* @return columns of all mapping strategies without auto-generated keys, as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getInsertableColumns() {
return Collections.unmodifiableSet(insertableColumns);
}
/**
* Gives columns that can be updated: columns minus keys
* @return columns of all mapping strategies without getKey(), as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getUpdatableColumns() {
return Collections.unmodifiableSet(updatableColumns);
}
/**
* Gives columns that can be selected
* @return columns of all mapping strategies, as an immutable {@link Set}
*/
@Override
public Set<Column<T, ?>> getSelectableColumns() {
return Collections.unmodifiableSet(selectableColumns);
}
@Override
public IdMapping<C, I> getIdMapping() {
return idMapping;
}
public Collection<ShadowColumnValueProvider<C, T>> getShadowColumnsForInsert() {
return Collections.unmodifiableCollection(this.mapping.getShadowColumnsForInsert());
}
public Collection<ShadowColumnValueProvider<C, T>> getShadowColumnsForUpdate() {
return Collections.unmodifiableCollection(this.mapping.getShadowColumnsForUpdate());
}
public void addShadowColumns(DefaultEntityMapping<C, I, T> entityMapping) {
entityMapping.mapping.getShadowColumnsForInsert().forEach(this::addShadowColumnInsert);
entityMapping.mapping.getShadowColumnsForUpdate().forEach(this::addShadowColumnUpdate);
}
@Override
public void addShadowColumnInsert(ShadowColumnValueProvider<C, T> valueProvider) {
// we delegate value computation to the default mapping strategy
mapping.addShadowColumnInsert(valueProvider);
// we must register it as an insertable column so we'll generate the right SQL order
insertableColumns.addAll(valueProvider.getColumns());
}
@Override
public void addShadowColumnUpdate(ShadowColumnValueProvider<C, T> valueProvider) {
// we delegate value computation to the default mapping strategy
mapping.addShadowColumnUpdate(valueProvider);
// we must register it as an insertable column so we'll generate the right SQL order
updatableColumns.addAll(valueProvider.getColumns());
}
@Override
public void addPropertySetByConstructor(PropertyAccessPoint<C, ?> accessor) {
mapping.addPropertySetByConstructor(accessor);
}
@Override
public <O> void addShadowColumnSelect(Column<T, O> column) {
mapping.addShadowColumnSelect(column);
fillSelectableColumns();
}
@Override
public Map<Column<T, ?>, ?> getInsertValues(C c) {
Map<Column<T, ?>, Object> insertValues = mapping.getInsertValues(c);
getVersionedKeyValues(c).entrySet().stream()
// autoincrement columns mustn't be written
.filter(entry -> !entry.getKey().isAutoGenerated())
.forEach(entry -> insertValues.put(entry.getKey(), entry.getValue()));
return insertValues;
}
@Override
public Map<UpwhereColumn<T>, ?> getUpdateValues(C modified, C unmodified, boolean allColumns) {
Map<UpwhereColumn<T>, Object> toReturn;
if (modified != null && unmodified != null && !getId(modified).equals(getId(unmodified))) {
// entities are different, so there's no value to be updated
toReturn = new HashMap<>();
} else {
toReturn = mapping.getUpdateValues(modified, unmodified, allColumns);
if (!toReturn.isEmpty()) {
if (allColumns) {
Set<Column<T, ?>> missingColumns = new HashSet<>(getUpdatableColumns());
missingColumns.removeAll(UpwhereColumn.getUpdateColumns(toReturn).keySet());
for (Column<T, ?> missingColumn : missingColumns) {
toReturn.put(new UpwhereColumn<>(missingColumn, true), null);
}
}
// Determining on which instance we should take where values : unmodified by default, modified for rough update (unmodified is null)
C whereSource = unmodified != null ? unmodified : modified;
for (Entry<Column<T, ?>, ?> entry : getVersionedKeyValues(whereSource).entrySet()) {
toReturn.put(new UpwhereColumn<>(entry.getKey(), false), entry.getValue());
}
}
}
return toReturn;
}
/**
* Build columns that can be inserted: columns minus generated keys
*/
private void fillInsertableColumns() {
insertableColumns.clear();
addInsertableColumns(mapping);
insertableColumns.addAll(getIdMapping().<T>getIdentifierAssembler().getColumns());
if (versioningMapping != null) {
insertableColumns.add(versioningMapping.getRight());
}
// NB : generated keys are never inserted but left because DMLGenerator needs its presence to detect it
// and also it prevents to generate empty statement when they are Alone in the Dark ;)
}
private void addInsertableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
insertableColumns.addAll(embeddedBeanMapping.getWritableColumns());
}
/**
* Build columns that can be updated: columns minus keys
*/
private void fillUpdatableColumns() {
updatableColumns.clear();
addUpdatableColumns(mapping);
if (versioningMapping != null) {
updatableColumns.add(versioningMapping.getRight());
}
// keys are never updated
updatableColumns.removeAll(getTargetTable().getPrimaryKey().getColumns());
updatableColumns.removeIf(Column::isAutoGenerated);
}
private void addUpdatableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
updatableColumns.addAll(embeddedBeanMapping.getWritableColumns());
}
private void fillSelectableColumns() {
selectableColumns.clear();
addSelectableColumns(mapping);
selectableColumns.addAll(getIdMapping().<T>getIdentifierAssembler().getColumns());
if (versioningMapping != null) {
selectableColumns.add(versioningMapping.getRight());
}
}
private void addSelectableColumns(EmbeddedBeanMapping<?, T> embeddedBeanMapping) {
selectableColumns.addAll(embeddedBeanMapping.getColumns());
}
@Override
public Map<Column<T, ?>, ?> getVersionedKeyValues(C c) {
Map<Column<T, ?>, Object> toReturn = new HashMap<>();
toReturn.putAll(getIdMapping().<T>getIdentifierAssembler().getColumnValues(getId(c)));
if (versioningMapping != null) {
toReturn.put(versioningMapping.getRight(), versioningMapping.getLeft().get(c));
}
return toReturn;
}
@Override
public Iterable<Column<T, ?>> getVersionedKeys() {
// we return a stable set to keep tests stable, shouldn't impact performances
Set<Column<T, ?>> columns = new KeepOrderSet<>();
if (versioningMapping != null) {
columns.add(versioningMapping.getRight());
}
columns.addAll(getTargetTable().getPrimaryKey().getColumns());
return Collections.unmodifiableSet(columns);
}
@Override
public I getId(C c) {
return getIdMapping().getIdAccessor().getId(c);
}
@Override
public void setId(C c, I identifier) {
getIdMapping().getIdAccessor().setId(c, identifier);
}
@Override
public boolean isNew(C c) {
return getIdMapping().isNew(c);
}
@Override
public C transform(ColumnedRow row) {
return getRowTransformer().transform(row);
}
@Override
public RowTransformer<C> getRowTransformer() {
return new RowTransformer<C>() {
@Override
public C transform(ColumnedRow row) {
C toReturn = mapping.getRowTransformer().transform(row);
// fixing identifier
if (!identifierSetByBeanFactory) {
setId(toReturn, getIdMapping().getIdentifierAssembler().assemble(row));
}
return toReturn;
}
@Override
public C newBeanInstance(ColumnedRow row) {
return mapping.transform(row);
}
@Override
public void applyRowToBean(ColumnedRow row, C bean) {
mapping.getRowTransformer().applyRowToBean(row, bean);
}
@Override
public void addTransformerListener(TransformerListener<? extends C> listener) {
mapping.addTransformerListener((TransformerListener<C>) listener);
}
};
}
@Override
public void addTransformerListener(TransformerListener<C> listener) {
getRowTransformer().addTransformerListener(listener);
}
}